package expression;

import java.util.HashMap;
import java.util.LinkedList;

public abstract class Fn {

	LinkedList<Fn> child = new LinkedList<Fn>();

	// CHILDREN

	public final Fn addParam(Fn fn) throws ExpressionException {
		if (!isAcceptableChild(fn))
			throw new ExpressionException();
		if (child.size() < getNParam()) {
			child.addFirst(fn);
			return fn;
		}
		return null;
	}

	public abstract int getNParam();

	protected abstract boolean isAcceptableChild(Fn fn);

	protected abstract boolean isAcceptableParam(Object o);

	protected abstract int precedence();

	private final int comparePrecedence(Fn fn2) {
		return (int) Math.signum(precedence() - fn2.precedence());
	}

	// EVALUATION

	public HashMap<String, Object> getVariableMap() {
		HashMap<String, Object> out = new HashMap<String, Object>();
		if (child != null)
			for (Fn fn : child)
				if (fn instanceof FnVariable)
					out.put(fn + "", 0);
				else
					out.putAll(fn.getVariableMap());
		return out;
	}

	protected abstract Object value(LinkedList<Object> param);

	public final Object evaluate(HashMap<String, Object> varSub) throws ExpressionException {
		LinkedList<Object> param = new LinkedList<Object>();
		Object o;
		if (child != null)
			for (Fn fn : child)
				if (fn instanceof FnVariable) {
					o = ((FnVariable) fn).value(varSub);
					if (!isAcceptableParam(o))
						throw new ExpressionException();
					param.add(o);
				} else if (fn instanceof FnBoolConstant) {
					o = ((FnBoolConstant) fn).value();
					param.add(o);
				} else if (fn instanceof FnNumberConstant) {
					o = ((FnNumberConstant) fn).value();
					param.add(o);
				} else if (fn instanceof FnStringConstant) {
					o = ((FnStringConstant) fn).value();
					param.add(o);
				} else {
					o = fn.evaluate(varSub);
					param.add(o);
				}
		return value(param);
	}

	// STRINGS

	@Override
	public String toString() {
		return toString(false);
	}

	public final String toString(boolean forceParenthesis) {
		String out = (forceParenthesis ? "(" : "") + prefix();
		if (child != null)
			for (Fn fn : child)
				out += fn.toString(comparePrecedence(fn) > 0) + separator();
		out = out.substring(0, out.length() - separator().length()) + postfix();
		return out + (forceParenthesis ? ")" : "");
	}

	public abstract String symbol();

	protected abstract String[] notation();

	private final String prefix() {
		return notation()[0];
	}

	private final String separator() {
		return notation()[1];
	}

	private final String postfix() {
		return notation()[2];
	}
}
